package com.environment.component;

import com.environment.Environment;
import com.environment.component.EffectComponent;
import com.environment.component.SkyComponent;
import com.environment.exception.MissingComponentException;
import com.environment.unit.Component;
import com.environment.unit.IComponent;
import com.jme.animation.SpatialTransformer;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.light.DirectionalLight;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.Skybox;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;

/**
 * <code>AstronomyComponent</code> extends <code>Component</code> and maintains all
 * astronomy entities of <code>Environment</code>.
 * <p>
 * The maintained entities include sun, moon, stars, sunlight and moonlight.
 * <p>
 * Length of day, dawn color, moon color, sun Texture, moon Texture, and star Texture
 * array have to set prior to connecting to <code>Environment</code>.
 * <p>
 * <code>AstronomyComponent</code> has to be connected to <code>SkyComponent</code>
 * before initialization.
 * <p>
 * In order for special effects such as lens flare and full screen blooming to affect
 * the astronomy entities, <code>AstronomyComponent</code> needs to be connected to
 * <code>EffectComponent<code> before connecting to <code>Environment</code>.
 * 
 * @author Yi Wang (Neakor)
 * @version Creation date: 04-10-2008 20:52 EST
 * @version Modified date: 06-15-2008 20:25 EST
 */
public class AstronomyComponent extends Component{
	/**
	 * Serial version.
	 */
	private static final long serialVersionUID = 1391808708958114187L;
	/**
	 * The reference to <code>Environment</code> instance.
	 */
	private Environment environment;
	/**
	 * The reference to <code>SkyComponent</code> instance.
	 */
	private SkyComponent sky;
	/**
	 * The reference to <code>EffectComponent</code> instance.
	 */
	private EffectComponent effect;
	/**
	 * The length of a complete day night cycle.
	 */
	private float length;
	/**
	 * The float array represents the sunlight color at dawn and dusk.
	 */
	private float[] dColor;
	/**
	 * The float array represents the moonlight color during night time.
	 */
	private float[] mColor;
	/**
	 * The astronomy <code>LightState</code>.
	 */
	private LightState lightState;
	/**
	 * The astronomy <code>SpatialTransformer</code> animator.
	 */
	private SpatialTransformer animator;
	/**
	 * The array of <code>Texture</code> for stars SkyBox.
	 */
	private Texture[] starTextures;
	/**
	 * The stars <code>SkyBox</code>.
	 */
	private Skybox stars;
	/**
	 * The sun <code>Texture</code>.
	 */
	private Texture sunTexture;
	/**
	 * The sun <code>Node</code>.
	 */
	private Node sun;
	/**
	 * The sun <code>DirectionalLight</code>.
	 */
	private DirectionalLight sunlight;
	/**
	 * The moon <code>Texture</code>.
	 */
	private Texture moonTexture;
	/**
	 * The moon <code>DirectionalLight</code>.
	 */
	private DirectionalLight moonlight;
	/**
	 * The temporary <code>Vector3f</code> light direction.
	 */
	private final Vector3f tempDirection;
	/**
	 * The temporary color float array for update.
	 */
	private final float[] tempColor;
	/**
	 * The temporary global ambient <code>ColorRGBA</code>.
	 */
	private final ColorRGBA tempAmbient;

	/**
	 * Constructor of <code>AstronomyComponent</code>
	 */
	public AstronomyComponent() {
		super(AstronomyComponent.class.toString());
		this.dColor = new float[4];
		this.mColor = new float[4];
		this.tempDirection = new Vector3f();
		this.tempColor = new float[4];
		this.tempAmbient = new ColorRGBA(1,1,1,1);
	}

	@Override
	public boolean validate() throws MissingComponentException{
		if(this.sky == null) {
			throw new MissingComponentException(SkyComponent.class.toString());
		}
		return true;
	}

	@Override
	protected void initializeDetail() {
		Node node = new Node("Animated");
		this.setLocalTranslation(new Vector3f());
		this.buildAstronomyLight();
		node.attachChild(this.buildSun());
		this.buildSunlight();
		node.attachChild(this.buildMoon());
		this.buildMoonlight();
		this.attachChild(this.buildStars());
		this.buildAstronomyAnim(node);
		this.attachChild(node);
	}
	
	/**
	 * Build the astronomy <code>LightState</code>.
	 */
	private void buildAstronomyLight() {
		this.lightState = this.environment.getRenderer().createLightState();
		this.lightState.setGlobalAmbient(this.tempAmbient);
		this.lightState.setTwoSidedLighting(true);
	}
	
	/**
	 * Build the sun <code>Quad</code> with lens flare effect.
	 * @return The <code>Node</code> contains the sun and its lens flare effect.
	 */
	private Node buildSun() {
		// Create the sun node.
		this.sun = new Node("Sun");
		this.sun.setLocalTranslation(new Vector3f(0, -0.9f*this.sky.getSkyRadius(), 0));
		// Create the sun quad.
		Quad sunQuad = new Quad("SunQuad", this.sky.getSkyRadius()/3, this.sky.getSkyRadius()/3);
		sunQuad.setModelBound(new BoundingBox());
		sunQuad.updateModelBound();
		sunQuad.setLocalTranslation(new Vector3f(0, 0, 0));
		TextureState textureState = this.environment.getRenderer().createTextureState();
		textureState.setTexture(this.sunTexture);
		sunQuad.setRenderState(textureState);
		sunQuad.setRenderState(this.environment.getAlphaState());
		Quaternion rotation = new Quaternion();
		rotation.fromAngleAxis(90*FastMath.DEG_TO_RAD, new Vector3f(1,0,0));
		sunQuad.setLocalRotation(rotation);
		sunQuad.setLightCombineMode(Spatial.LightCombineMode.Off);
		if(this.effect != null) this.effect.getBlooming().add(sunQuad);
		this.sun.attachChild(sunQuad);
		if(this.effect != null) this.sun.attachChild(this.effect.getLensFlare());
		// Return the sun node.
		return this.sun;
	}
	
	/**
	 * Build the sunlight.
	 */
	private void buildSunlight() {
		this.sunlight = new DirectionalLight();
		this.sunlight.setEnabled(true);
		this.sunlight.setDiffuse(new ColorRGBA(0,0,0,0));
		this.sunlight.setAttenuate(false);
		this.sunlight.setShadowCaster(true);
		this.lightState.attach(this.sunlight);
	}
	
	/**
	 * Build the moon <code>Quad</code>.
	 * @return The <code>Node</code> contains the moon <code>Quad</code>.
	 */
	private Node buildMoon() {
		// Create the moon node.
		Node moon = new Node("Moon");
		moon.setLocalTranslation(new Vector3f(0, 0.9f*this.sky.getSkyRadius(), 0));
		// Create the moon.
		Quad moonQuad = new Quad("MoonQuad", this.sky.getSkyRadius()/8, this.sky.getSkyRadius()/8);
		moonQuad.setModelBound(new BoundingBox());
		moonQuad.updateModelBound();
		moonQuad.setLocalTranslation(new Vector3f(0, 0, 0));
		TextureState textureState = this.environment.getRenderer().createTextureState();
		textureState.setTexture(this.moonTexture);
		moonQuad.setRenderState(textureState);
		moonQuad.setRenderState(this.environment.getAlphaState());
		Quaternion rotation = new Quaternion();
		rotation.fromAngleAxis(90*FastMath.DEG_TO_RAD, new Vector3f(1,0,0));
		moonQuad.setLocalRotation(rotation);
		moonQuad.setLightCombineMode(Spatial.LightCombineMode.Off);
		if(this.effect != null) this.effect.getBlooming().add(moonQuad);
		// Attach the moon to the moon node.
		moon.attachChild(moonQuad);
		// Return the moon node.
		return moon;
	}
	
	/**
	 * Build the moonlight.
	 */
	private void buildMoonlight() {
		this.moonlight = new DirectionalLight();
		this.moonlight.setEnabled(true);
		this.moonlight.setDiffuse(new ColorRGBA(this.mColor[0], this.mColor[1], this.mColor[2], this.mColor[3]));
		this.moonlight.setAttenuate(false);
		this.moonlight.setShadowCaster(true);
		this.lightState.attach(this.moonlight);
	}
	
	/**
	 * Build the <code>Skybox</code> for the star effect.
	 * @return The star <code>Skybox</code> spatial.
	 */
	private Spatial buildStars() {
		float extent = 0.7f*this.sky.getSkyRadius();
		this.stars = new Skybox("StarBox", extent, extent, extent);
		this.stars.setLocalTranslation(new Vector3f(0,0,0));
		this.stars.setTexture(Skybox.Face.Up, this.starTextures[0]);
		this.stars.setTexture(Skybox.Face.North, this.starTextures[1]);
		this.stars.setTexture(Skybox.Face.East, this.starTextures[2]);
		this.stars.setTexture(Skybox.Face.South, this.starTextures[3]);
		this.stars.setTexture(Skybox.Face.West, this.starTextures[4]);
		this.stars.setTexture(Skybox.Face.Down, this.starTextures[5]);
		this.stars.getFace(Skybox.Face.Down).setDefaultColor(new ColorRGBA(0,0,0,0));
		this.stars.setRenderState(this.environment.getAlphaState());
		this.stars.updateRenderState();
		return this.stars;
	}
	
	/**
	 * Build the rotational astronomy animation.
	 * @param node The Node to be animated.
	 */
	private void buildAstronomyAnim(Node node) {
		this.animator = new SpatialTransformer(1);
		this.animator.setObject(node, 0, -1);
	    Quaternion midNight = new Quaternion();
	    midNight.fromAngleAxis(0, new Vector3f(0,0,1));
	    this.animator.setRotation(0, 0, midNight);
	    Quaternion noon = new Quaternion();
	    noon.fromAngleAxis(FastMath.DEG_TO_RAD*180, new Vector3f(0,0,1));
	    this.animator.setRotation(0, this.length/2, noon);
	    Quaternion finalMidNight = new Quaternion();
	    finalMidNight.fromAngleAxis(FastMath.DEG_TO_RAD*360, new Vector3f(0,0,1));
	    this.animator.setRotation(0, this.length, finalMidNight);
	    this.animator.setActive(true);
	    this.animator.setRepeatType(Controller.RT_WRAP);
	    this.animator.interpolateMissing();
	    node.addController(this.animator);
	}

	@Override
	protected void updateDetail(float time) {
		this.updateLightDirection();
		this.updateSunLightColor(time);
		this.updateMoonLightColor(time);
		this.updateAmbientColor();
		this.updateStarEffect(time);
	}
	
	/**
	 * Update the sunlight and moonlight direction based on the sun's position.
	 */
	private void updateLightDirection() {
		this.tempDirection.set(this.getWorldTranslation());
		this.tempDirection.subtractLocal(this.sun.getWorldTranslation());
		this.tempDirection.normalizeLocal();
		this.sunlight.setDirection(this.tempDirection);
		this.moonlight.setDirection(this.tempDirection.negate());
	}
	
	/**
	 * Update the sunlight color based on the current time.
	 * @param time The current time of the environment in 24 hour time system.
	 */
	private void updateSunLightColor(float time) {
		// If the current time is between 6:00 and 9:00, change the color towards white.
		if(time >= 6 && time < 9) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = this.dColor[i] + (1-this.dColor[i])*(time-6)/3;
			}
			this.setColor(this.sunlight.getDiffuse());
		// Day time.
		} else if(time >=9 && time < 15) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = 1;
			}
			this.setColor(this.sunlight.getDiffuse());
		// Else if the current time is between 15:00 and 18:00, change the color towards dColor.
		} else if(time >= 15 && time < 18) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = 1 + (this.dColor[i]-1)*(time-15)/3;
			}
			this.setColor(this.sunlight.getDiffuse());
		// Else if the current time is between 18:00 and 21:00, change the color towards gray.
		} else if(time >= 18 && time < 21) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = this.dColor[i] + (0.5f-this.dColor[i])*(time - 18)/3;
			}
			this.setColor(this.sunlight.getDiffuse());
		// Night time.
		} else if(time >= 21 || time < 3) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = 0.5f;
			}
			this.setColor(this.sunlight.getDiffuse());
		// Else if the current time is between 3:00 and 6:00, change the color towards dColor.
		} else if(time >= 3 && time < 6) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = 0.5f + (this.dColor[i]-0.5f)*(time - 3)/3;
			}
			this.setColor(this.sunlight.getDiffuse());
		}
	}
	
	/**
	 * Update the moonlight color based on the current time.
	 * @param time The current time of the environment in 24 hour time system.
	 */
	private void updateMoonLightColor(float time) {
		// If the current time is between 6:00 and 9:00, change the moonlight color towards gray.
		if(time >= 6 && time < 9) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = this.mColor[i] + (0.5f-this.mColor[i])*(time - 6)/3;
			}
			this.setColor(this.moonlight.getDiffuse());
		// Day time.
		} else if(time >= 9 && time < 15) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = 0.5f;
			}
			this.setColor(this.moonlight.getDiffuse());
		// Else if the current time is between 15:00 and 18:00, change the moonlight color towards the mColor.
		} else if(time >= 15 && time < 18) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = 0.5f + (this.mColor[i]-0.5f)*(time - 15)/3;
			}
			this.setColor(this.moonlight.getDiffuse());
		// Night time.
		} else if(time >= 18 || time < 6) {
			for(int i = 0; i < this.tempColor.length; i++) {
				this.tempColor[i] = this.mColor[i];
			}
			this.setColor(this.moonlight.getDiffuse());
		}
	}
	
	/**
	 * Update the ambient light color based on the sunlight and moonlight colors.
	 */
	private void updateAmbientColor() {
		this.tempColor[0] = this.sunlight.getDiffuse().r + this.moonlight.getDiffuse().r;
		this.tempColor[1] = this.sunlight.getDiffuse().g + this.moonlight.getDiffuse().g;
		this.tempColor[2] = this.sunlight.getDiffuse().b + this.moonlight.getDiffuse().b;
		this.tempColor[3] = 1;
		this.setColor(this.tempAmbient);
		this.lightState.setGlobalAmbient(this.tempAmbient);
	}
	
	/**
	 * Update the alpha of all batches of star effect dome based on the current time.
	 * @param time The current time of the environment in 24 hour time system.
	 */
	private void updateStarEffect(float time) {
		// If the current time is between 20:00 and 24:00, increase the alpha of all sides toward 1.
		if(time >= 20 && time < 24) {
			this.setStarAlpha((time - 20.0f)/4.0f);
		// Else if the current time is between 0:00 and 4:00, decrease the alpha of all sides toward 0.
		} else if(time >= 0 && time < 4) {
			this.setStarAlpha(1.0f - (time - 0.0f)/4.0f);
		// Else if the current time is equal or greater than 4:00 or less than 20:00, set the alpha to 0.
		} else if(time >= 4 || time < 20) {
			this.setStarAlpha(0);
		}
	}
	
	/**
	 * Set the given <code>ColorRGBA</code> to the temporary color.
	 * @param color The <code>ColorRGBA</code> to be set.
	 */
	private void setColor(ColorRGBA color) {
		color.r = this.tempColor[0];
		color.g = this.tempColor[1];
		color.b = this.tempColor[2];
		color.a = this.tempColor[3];
		color.clamp();
	}
	
	/**
	 * Set the alpha value of the star effect to the star <code>Skybox</code>.
	 * @param alpha The alpha value to be set.
	 */
	private void setStarAlpha(float alpha) {
		this.stars.getFace(Skybox.Face.Up).getDefaultColor().a = alpha;
		this.stars.getFace(Skybox.Face.North).getDefaultColor().a = alpha;
		this.stars.getFace(Skybox.Face.East).getDefaultColor().a = alpha;
		this.stars.getFace(Skybox.Face.South).getDefaultColor().a = alpha;
		this.stars.getFace(Skybox.Face.West).getDefaultColor().a = alpha;
	}
	
	@Override
	public void connect(IComponent component) {
		if(component instanceof Environment) {
			this.environment = (Environment)component;
		} else if(component instanceof SkyComponent) {
			this.sky = (SkyComponent)component;
		} else if(component instanceof EffectComponent) {
			this.effect = (EffectComponent)component;
		}
	}

	/**
	 * Set the length of a complete day night cycle.
	 * @param length The length of a complete day night cycle.
	 */
	public void setLengthOfDar(float length) {
		this.length = length;
	}
	
	/**
	 * Set the sunlight <code>ColorRGBA</code> at dawn and dusk.
	 * @param color The sunlight <code>ColorRGBA</code> at dawn and dusk.
	 */
	public void setDawnColor(ColorRGBA color) {
		this.dColor[0] = color.r;
		this.dColor[1] = color.g;
		this.dColor[2] = color.b;
		this.dColor[3] = color.a;
	}
	
	/**
	 * Set the moonlight <code>ColorRGBA</code> during night time.
	 * @param color The moonlight <code>ColorRGBA</code> during night time.
	 */
	public void setMoonColor(ColorRGBA color) {
		this.mColor[0] = color.r;
		this.mColor[1] = color.g;
		this.mColor[2] = color.b;
		this.mColor[3] = color.a;
	}
	
	/**
	 * Set the sun <code>Texture</code>.
	 * @param texture The <code>Texture</code> used by the sun.
	 */
	public void setSunTexture(Texture texture) {
		this.sunTexture = texture;
	}
	
	/**
	 * Set the moon <code>Texture</code>.
	 * @param texture The <code>Texture</code> used by the moon.
	 */
	public void setMoonTexture(Texture texture) {
		this.moonTexture = texture;
	}
	
	/**
	 * Set the array of six <code>Texture</code> used by the stars SkyBox. The order
	 * of the <code>Texture</code> is "Up", "North", "East", "South", "West" and "Down".
	 * @param textures The array of six <code>Texture</code> used by the stars SkyBox.
	 */
	public void setStarTextures(Texture[] textures) {
		this.starTextures = textures;
	}
	
	/**
	 * Retrieve the length of a complete day night cycle in seconds.
	 * @return The length of a complete day night cycle in seconds.
	 */
	public float getLengthOfDay() {
		return this.length;
	}
	
	/**
	 * Retrieve the astronomy <code>LightState</code>.
	 * @return The astronomy <code>LightState</code>.
	 */
	public LightState getAstronomyLight() {
		return this.lightState;
	}

	/**
	 * Retrieve the global ambient color.
	 * @return The <code>ColorRGBA</code> global ambient.
	 */
	public ColorRGBA getGlobalAmbient() {
		return this.lightState.getGlobalAmbient();
	}

	/**
	 * Retrieve the astronomy animator.
	 * @return The astronomy animator.
	 */
	public SpatialTransformer getAnimatior() {
		return this.animator;
	}
}
